¿Cómo y dónde se producen los accidentes en Madrid?

Autores/as

Gema Fernández-Avilés (Gema.FAviles@uclm.es)

Isidro Hidalgo (Isidro.Hidalgo@uclm.es)

Fecha de publicación

20 de enero de 2025

Nota

Los datos que se utilizan en esta historia están disponibles en el paquete CDR que puede instalarse con el siguiente comando (se comprueba si no lo está):

if (!require(CDR)){
  if (!require(remotes)) {install.packages("remotes")}
  remotes::install_github("cdr-book/CDR")
  }

Accidentes de tráfico en la Ciudad de Madrid registrados por Policía Municipal con víctimas y/o daños al patrimonio en el 2020. Los datos se han descargado del Portal de datos abiertos del Ayuntamiento de Madrid.)

1 Cómo y dónde se producen los accidentes en Madrid

Esta mañana estaba tan tranquilo en mi oficina (trabajo en el distrito de Chamartín para el Ayuntamiento) y me sueltan de repente: “el jefe quiere verte”. Lo primero que he pensado es que me echaban, como soy un poco “cansa almas”… Pero no, entro en el despacho del jefe… ¡Y allí está Almeida! Que ha oído que he hecho un curso de visualización en la UCLM y me he convertido en un crack de la visualización. Yo no sabía donde meterme, pero he mantenido perfectamente la compostura, como un profesional.

Así que ya me ha ido contando que está muy preocupado con los accidentes y quiere analizar el tema, para buscar soluciones. Que me agradecería mucho si encontramos dónde está el problema, para bajar el número y gravedad de los accidentes en la ciudad. El problema, me dice, es que solo me puede facilitar los datos del año 2020. Ya le he avisado de que es un año muy particular, por el estado de alarma, pero algo sacaremos…

2 Entender el contexto

Cómo definir el propósito y la audiencia de tu análisis

¿Qué le interesa al alcalde? Bajar la tasa de accidentes. Pero desde el punto de vista de la visualización, está claro que va a necesitar información georreferenciada, por lo que haremos mapas. Por otro lado, seguro que la dimensión es importante, porque no es lo mismo Madrid en agosto que un día de lluvia en calendario escolar…

En cualquier caso, lo primero es hacer un análisis exploratorio para ver si hay algo raro en los datos y analizar mínimamente las variables de interés. Y después, le vamos a preparar los gráficos necesarios para aportarle conocimiento sobre los accidentes en la ciudad.

library(CDR)               # Carga el paquete CDR para la lectura de datos
library(tidyverse)         # Manipulación y visualización de datos
library(sf)                # Para trabajar con datos espaciales
library(mapSpain)          # Para obtener y visualizar mapas de España
library(tidyterra)         # Para trabajar con datos espaciales y raster
library(lubridate)         # Manipulación de fechas y horas
library(data.table)        # Para manipulación eficiente de datos
library(gplots)            # Carga el paquete gplots para crear gráficos avanzados
library(RColorBrewer)      # Carga el paquete RColorBrewer para paletas de colores
library(leaflet)           # Carga el paquete leaflet para crear mapas interactivos
library(leaflet.extras)    # Carga el paquete leaflet.extras para funcionalidades adicionales en leaflet
library(ggmosaic)          # Carga el paquete ggmosaic para crear gráficos de mosaico

3 Elegir una visualización adecuada

Selección de gráficos y visualizaciones que mejor representen tus datos.

3.1 Almeida quiere saber cómo y dónde se producen los accidentes en Madrid

Ejercicio 1 ¿Dónde se han producido los accidentes de tráfico en Madrid en el 2020?

data(accidentes2020_data)
head(accidentes2020_data)
   num_expediente      fecha     hora
1:    2019S040008 07/09/2020 23:00:00
2:    2019S040008 07/09/2020 23:00:00
3:    2020S000001 01/01/2020  1:15:00
4:    2020S000001 01/01/2020  1:15:00
5:    2020S000002 01/01/2020  1:20:00
6:    2020S000002 01/01/2020  1:20:00
                                   localizacion numero            distrito
1:                    CALL. SAN MAXIMILIANO, 38     38       CIUDAD LINEAL
2:                    CALL. SAN MAXIMILIANO, 38     38       CIUDAD LINEAL
3: AVDA. CANILLEJAS A VICALVARO / CALL. SILFIDE      1 SAN BLAS-CANILLEJAS
4: AVDA. CANILLEJAS A VICALVARO / CALL. SILFIDE      1 SAN BLAS-CANILLEJAS
5:                            CALL. SILVANO, 31     31           HORTALEZA
6:                            CALL. SILVANO, 31     31           HORTALEZA
                 tipo_accidente estado_meteorológico tipo_vehiculo tipo_persona
1: Choque contra obstáculo fijo            Despejado       Turismo    Conductor
2: Choque contra obstáculo fijo            Despejado VMU eléctrico    Conductor
3:      Colisión fronto-lateral                 NULL       Turismo    Conductor
4:      Colisión fronto-lateral                 NULL       Turismo    Conductor
5: Choque contra obstáculo fijo            Despejado       Turismo    Conductor
6: Choque contra obstáculo fijo            Despejado       Turismo     Pasajero
        rango_edad   sexo lesividad coordenada_x_utm coordenada_y_utm
1: De 21 a 24 años Hombre      NULL         444597.8          4475156
2: De 25 a 29 años  Mujer      NULL         444597.8          4475156
3: De 18 a 20 años Hombre      NULL         447710.6          4477156
4: De 30 a 34 años Hombre      NULL         447710.6          4477156
5: De 21 a 24 años Hombre      NULL         445076.3          4478372
6: De 40 a 44 años Hombre      NULL         445076.3          4478372
   positiva_alcohol positiva_droga
1:                N           NULL
2:                N           NULL
3:                N           NULL
4:                S           NULL
5:                N           NULL
6:                N           NULL
ggplot(data = accidentes2020_data,                          # Crea un gráfico con el dataset accidentes2020_data
       aes(x = coordenada_x_utm, y = coordenada_y_utm)) +   # x= coordenada_x_utm e y= coordenada_y_utm  
  geom_point() +                                            # Añade puntos al gráfico para cada observación 
  coord_fixed()                                             # Mantiene una relación fija entre las unidades del eje x y el eje y

3.2 El desorden es tu enemigo

Deja mucho que desear, ¿verdad?: no tenemos información, solo unos datos representados en un gráfico. Con muy poco, tirando de estética (reduciendo la dimensión de los puntos para que se vea el fondo) y alguna referencia geográfica (en forma de mapa de carreteras), veremos cómo se reduce la carga cognitiva:

3.2.1 Paso 1: # Convierte el dataset accidentes2020_data a un objeto sf

accidentes2020_sf <- st_as_sf(accidentes2020_data,     
  coords = c("coordenada_x_utm", "coordenada_y_utm"),  # Define las columnas de coordenadas UTM
  crs = 25830                                          # Establece el sistema de referencia de coordenadas a ETRS89/UTM zona 30N, usado en Europa
)

3.2.2 Paso 2: Obtiene datos del municipio de Madrid con la función esp_get_munic de la libreíra mapSpain

madrid <- esp_get_munic(munic = "^Madrid$") |>    # Obtiene los datos del municipio de Madrid
  st_transform(25830)                             # Transforma las coordenadas al sistema de referencia ETRS89/UTM zona 30N

# Descarga una imagen de un mapa estático de las carreteras de Madrid
tile <- esp_getTiles(madrid, "IDErioja", zoommin = 2)

3.2.3 Paso 3: Primer mapa mejorado

ggplot() +                                     # Inicia un gráfico vacío con ggplot
  geom_spatraster_rgb(data = tile) +           # Añade la imagen del mapa estático como fondo
  geom_sf(data = accidentes2020_sf,            # Añade los datos de accidentes como puntos en el mapa
    col = "blue", size = 0.05, alpha = 0.3) +  # Define el color, tamaño y transparencia de los puntos
  coord_sf(expand = FALSE) +                   # Mantiene las proporciones de las coordenadas sin expansión
  labs(title = "¿Dónde se producen los accidentes de tráfico en Madrid?")   

¡Esto ya es otra cosa! Apreciamos, lógicamente, la concentración de accidentes en el centro de la ciudad y, especialmente, en las arterias principales.

3.3 El cuándo

Además, como hemos comentado, tenemos conocimiento a priori sobre la influencia de la fecha en el aumento de tráfico, por lo que probablemente encontremos alguna influencia de la dimensión temporal. Vamos a verlo…

Ejercicio 2 ¿Cuándo se han producido los accidentes?

ggplot(data = accidentes2020_data,
       aes(x = fecha, y = hora)) + 
  geom_point() +
  theme_minimal()

3.4 Eliminemos de nuevo el desorden

Vamos a separar la fecha y la hora, porque en ese gráfico no vemos casi nada…

3.4.1 Paso 1: Prepara los datos teniendo en cuenta la timensión temporal

# Convertir la columna 'fecha' a formato Date
accidentes2020_data$fecha <- as.Date(accidentes2020_data$fecha, format = "%d/%m/%Y")

accidentes2020_fecha <- accidentes2020_data |>  # Crea el dataset accidentes2020_fecha
  select(fecha) |>                              # Selecciona la columna fecha
  group_by(fecha) |>                            # Agrupa los datos por fecha
  mutate(n = n()) |>                            # Crea una nueva columna n con el conteo de accidentes por fecha
  unique()                                      # Elimina duplicados, manteniendo una fila por fecha con el conteo

3.4.2 Paso 2: Gráfico del cuándo según el día

ggplot(data = accidentes2020_fecha,   # Crea un gráfico con el dataset accidentes2020_fecha
       aes(x = fecha, y = n)) +       # Define las estéticas del gráfico, con fecha en el eje x y n en el eje y
  geom_line() +                       # Añade una línea para representar el número de accidentes por fecha
  theme_minimal()                     # Aplica un tema minimalista al gráfico

Como ya le advertimos al alcalde, el año 2020 no es bueno para sacar conclusiones. Ya le hemos pedido que nos facilite datos más amplios para poder proporcionarle una mejor visualización del número de accidentes por fecha, pero mientras tanto, nos tendremos que arreglar con lo que tenemos…

3.4.3 Gráfico del cuándo según la hora

# Convierte la columna hora a formato POSIXct y extrae la hora del día
accidentes2020_data$hora_dia <- hour(as.POSIXct(accidentes2020_data$hora, format = "%H:%M:%S"))  

 
accidentes2020_hora_dia <- accidentes2020_data |>  # Crea el dataset accidentes2020_hora_dia
  select(hora_dia) |>                              # Selecciona la columna hora_dia
  group_by(hora_dia) |>                            # Agrupa los datos por hora_dia
  mutate(n = n()) |>                               # Crea una nueva columna n con el conteo de accidentes por hora
  unique()                                         # Elimina duplicados

ggplot(data = accidentes2020_hora_dia,             # Crea un gráfico con el dataset accidentes2020_hora_dia
       aes(x = hora_dia, y = n)) +                 # x= hora_dia e  y=n  
  geom_line(linewidth = 1, color = "orange") +     # Añade una línea de color naranja y ancho de línea 1
  labs(title = "Número de accidentes según la franja horaria del día",   
       subtitle = "Las 14 y las 19 horas presentan el mayor número de accidentes",   
       x = "Hora del día", 
       y = "Número de accidentes en la franja horaria") +   
  scale_y_continuous(labels = function(x) {  # Formatea las etiquetas del eje y
    format(x,
      big.mark = ".", 
      decimal.mark = ",", 
      scientific = FALSE  # Usa puntos como separadores de miles y comas como separadores decimales
    )
  }) +
  theme_minimal()   

Es evidente que la hora punta a mediodía y en la salida del trabajo, a las 14 y las 19 horas del día son los 2 picos a vigilar. Pero este gráfico tiene un gran margen de mejora.

3.5 El tipo de accidente

Ejercicio 3 ¿De qué tipo son los accidentes?

3.5.1 Paso 1: Resumen numérico por tipo de accidente

accidentes2020_data <- accidentes2020_data |>
  mutate_if(is.character, as.factor)

accidentes2020_data |>
  count(tipo_accidente) |>
  mutate(porcentaje = 100 * n / sum(n))
                  tipo_accidente    n porcentaje
 1:                      Alcance 7294 22.4936010
 2:           Atropello a animal   75  0.2312887
 3:          Atropello a persona 2127  6.5593487
 4:                        Caída 2118  6.5315940
 5: Choque contra obstáculo fijo 4667 14.3923274
 6:             Colisión frontal  899  2.7723810
 7:      Colisión fronto-lateral 8081 24.9205909
 8:             Colisión lateral 4386 13.5257656
 9:            Colisión múltiple 2231  6.8800691
10:                Despeñamiento    2  0.0061677
11:                         Otro  251  0.7740463
12:        Solo salida de la vía  151  0.4656613
13:                       Vuelco  145  0.4471582

Las tablas también son elementos de visualización. En este caso, hay algo evidente que hacer, pero lo vamos a explicar con el siguiente gráfico:

3.5.2 Paso 2: Gráfico de barras por tipo de accidente

accidentes2020_data |>
  ggplot() +
  geom_bar(aes(y=tipo_accidente), 
           fill = "orange", 
           alpha = 0.6) +
  theme_bw()

¿Qué le falta a este gráfico?¿En qué se puede mejorar? Pues está muy claro: ordenar las categorías. Si no están ordenadas tardamos mucho en determinar, por ejemplo, cuál es la tercera, o la quinta… Únicamente la más importante es relativamente fácil de situar. Pero veamos cuando ordenamos las categorías:

3.5.3 Paso 3: Gráfico de barras por tipo de accidente ordenado

ggplot(accidentes2020_data, aes(y = reorder(tipo_accidente,  
                                            -tipo_accidente,  # Reordena los tipos de accidente en orden descendente
                                            length))) +       # Usa la longitud para el reordenamiento
  geom_bar(fill = "orange", alpha = 0.6) +                    # Añade barras al gráfico con color naranja y transparencia
  labs(title = "Tipos de accidente en Madrid durante el año 2020",   
       subtitle = "Sobresalen la colisión fronto-lateral y el alcance",   
       x = "Número de accidentes", 
       y = "") +  
  scale_x_continuous(labels = function(x) {                   # Formatea las etiquetas del eje x
    format(x,
      big.mark = ".", 
      decimal.mark = ",", 
      scientific = FALSE  
    )
  }) +
  theme_bw()   

3.6 Visualizaciones específicas georreferenciadas

Una categoría especial, y que merece mucho la pena destacar, cuando tenemos información georreferenciada, son los mapas interactivos. Una de las librerías más populares para construirlos es ‘leaflet’, que usa los mapas de OpenStreetMap, permitiendo incluir los mapas físicos de ESRI.

3.6.1 Paso 0: Preparación de los datos

# Transforma CRS de accidentes2020_sf a WGS 84 (CRS 4326)
accidentes2020_sf <- st_transform(accidentes2020_sf, crs = 4326)  

# Extrae las coordenadas de longitud y las añade como una nueva columna en el dataframe
accidentes2020_data$longitude <- st_coordinates(accidentes2020_sf)[, 1]  
accidentes2020_data$latitude <- st_coordinates(accidentes2020_sf)[, 2]

3.6.2 Paso 1: Primer mapa interactivo con leaflet

leaflet(data = accidentes2020_data) |>  # Crea un mapa interactivo con el dataset accidentes2020_data
  addTiles() |>                         # Añade las teselas (tiles) del mapa base
  addCircleMarkers(lng = ~longitude, lat = ~latitude,          # Añade marcadores circulares en lon y lat
                   radius = 1,                                 # Define el radio, 
                   color = "red",                              # Define el color   
                   opacity = 0.5                               # Define la opacidad de los marcadores
                   )   
#  addProviderTiles(providers$Esri.WorldImagery)

3.7 Piensa como un diseñador

Estas visualizaciones están muy bien pero… ¿y si pudiéramos aminorar la carga cognitiva del mapa interactivo? Una forma de hacerlo es usar agrupaciones. Veamos cómo hacerlo.

3.7.1 Paso 2: Primer mapa interactivo con leaflet y clusters

leaflet(data = accidentes2020_data) |>
  addTiles() |>
  addCircleMarkers(lng = ~longitude, lat = ~latitude, 
                   radius = 2, color = "red", 
                   opacity = 0.5, 
                   clusterOptions = markerClusterOptions()
                   )

Otra forma de visualizar que permite ‘leaflet’ son los mapas de calor:

3.7.2 Paso 3: Primer mapa interactivo con leaflet y mapa de calor

leaflet(data = accidentes2020_data) |>
  addTiles() |>
  addHeatmap(lng = ~longitude, lat = ~latitude, 
             blur = 50, max = 0.05)

Ejercicio 4 Esta visualización la podemos configurar con los parámetros. Es tu turno: juega con ellos y consigue una gradación e intensidad de colores a tu gusto.

leaflet(data = accidentes2020_data) |>
  addTiles() |>
  addHeatmap(lng = ~longitude, lat = ~latitude,
             blur = _____, max = _____)

4 Contar una historia

Cómo narrar una historia convincente con tus datos.

Hemos visto el planteamiento, en el que proporcionamos el contexto: el mismísimo alcalde de Madrid se ha enterado de nuestras excelentes capacidades y nos ha pedido ayuda…

El análisis exploratorio nos ha ido introduciendo en la trama, donde descubrimos que hay una fuerte dependencia de la hora del día y del lugar donde se produce el accidente.

Pero como siempre, lo más importante es el desenlace, donde le estamos dando al alcalde las conclusiones más interesantes, que podríamos resumir en un par de gráficos:

  • Con el gráfico de evolución del número de accidentes a lo largo del día va a saber que (es lógico) que en las 14 y 19 horas se producen el mayor número de accidentes.

  • Con un mapa interactivo (podemos elegir cuál nos proporciona la mejor visualización) le damos la posibilidad de mirar de forma dinámica qué zonas son las que necesitan más acciones de protección.

4.1 Para pensar:

Nosotros te hemos propuesto un par de visualizaciones, pero puede que no estés de acuerdo… ¿Qué visualizaciones utilizarías en el desenlace, para aportarle el mayor conocimiento a Almeida sobre los accidentes de tráfico de su ciudad?